Skip to content

fix(capture): preserve token capture after synthetic completions - #3670

Merged
ananthsub merged 3 commits into
mainfrom
pranav/vllm_token_limit_fix
Sep 29, 2026
Merged

ananthsub merged 3 commits into
mainfrom
pranav/vllm_token_limit_fix

Conversation

@pthombre

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes external token capture when Gym serves an empty synthetic completion after a vLLM context/token-limit error or the sequential-reasoning guard.

The served-response finalization introduced in #3301 correctly commits fingerprints after API conversion, but it also runs for responses that did not come from a normal worker completion. Two existing paths hit this case:

  • A backend HTTP 400 for context length or max_tokens is converted into an empty completion with finish_reason="length".
  • With sequential_reasoning_allowed=false, a continuation that hits the reasoning guard returns an empty completion with finish_reason="content_filter".

Neither path reaches the worker-response preparation hook, so neither has worker commit coordinates. Finalization currently records worker_response_missing_commit_coordinates, which can poison the entire rollout and discard valid tokens captured in earlier turns. For example, a successful tool-use turn followed by a context-limit completion loses its already-staged training row.

This PR adds a request-scoped external_worker_response_seen flag and sets it only when a normal worker completion reaches prepare_response(). If no worker completion was received, the shared finalizer returns without committing or poisoning the call. Existing middleware still records request_finished_without_staged_coordinates; a consumer selecting the preceding committed terminal can retain the valid token chain.

The fix lives in the shared handler introduced by #2823 and covers both vLLM and Megatron worker capture. A real worker completion missing its acknowledgement still fails with worker_response_missing_commit_coordinates; malformed acknowledgements and explicit worker capture failures retain their existing behavior. Sending a request alone does not set the flag, because the backend may return a context-limit error.

Adds 44 regression cases covering both backends, JSON and buffered SSE, Chat Completions/Responses/Messages/compaction routes, overflow before or after a committed call, and reasoning-only continuations. The tests verify lineage, preserved tokens/masks/logprobs, request isolation, and transport-field stripping. A synthetic response explicitly selected as the terminal remains unattributable; an initial overflow creates no trainable row.

No separate issue is needed for this focused correction to the existing capture lifecycle; the reproduction and affected behavior are documented here.

Validation

  • Slurm job 3966743: 1,055 tests passed, zero failures/errors/skips, using container-native Python 3.13.14 in the requested NeMo RL nightly image. Both changed test files were also overlaid on the unmodified base: all 44 new regression cases failed with the missing-coordinates behavior before the production fix.
  • Ruff lint, formatting, and changed-file pre-commit passed in that job. Targeted coverage exercised every added production line and both outcomes of the new finalizer guard.
  • pre-commit run --all-files passed before publishing, and git diff --check passed.
  • CI classification is full because shared core code changes. The full core/sandbox coverage gate and eight-shard server suite were not run locally; the focused capture/model-server suite and real rollouts were used for this patch. Targeted coverage does not establish the repository-wide coverage threshold.

Container used for both Slurm jobs:

/lustre/fsw/portfolios/nemotron/projects/nemotron_sw_post/users/pthombre/enroot-images/nvcr.io+nvidian+nemo-rl+nightly.squashfs
Validation commands and local artifacts

Job submissions from the fix worktree:

sbatch cache/worker-bypass-validation/native-validation.sbatch
sbatch cache/worker-bypass-validation/smoke.sbatch

The native validation runner invokes pytest through a bootstrap that selects the current checkout and asserts the imported Gym/model-server code comes from that checkout. Its passing test command uses this selection and configuration:

python cache/worker-bypass-validation/pytest_bootstrap.py \
  -o addopts= -o log_cli=false --import-mode=importlib -q --tb=short \
  tests/unit_tests/test_external_capture_handlers.py \
  tests/unit_tests/test_base_responses_api_model.py \
  tests/unit_tests/test_chat_completions_streaming.py \
  tests/unit_tests/test_responses_api_model_streaming.py \
  tests/unit_tests/test_token_id_capture.py \
  tests/unit_tests/test_token_capture*.py \
  responses_api_models/vllm_model/tests \
  responses_api_models/vllm_model_with_compaction/tests \
  --cov=nemo_gym.token_id_capture.sink \
  --cov=nemo_gym.token_id_capture.external_capture \
  --cov-report=term-missing --cov-fail-under=0 \
  --cov-config=cache/worker-bypass-validation/coverage.ini \
  --cov-report=xml:cache/worker-bypass-validation/coverage.xml \
  --junitxml=cache/worker-bypass-validation/regressions.xml

The temporary baseline checkout runs the two changed test files with -k synthetic_completion. The GPU smoke job runs python cache/worker-bypass-validation/real_worker_smoke.py.

All-files hooks were invoked with:

PRE_COMMIT_HOME="$PWD/cache/worker-bypass-validation/pr-pre-commit" \
UV_CACHE_DIR="$PWD/cache/worker-bypass-validation/uv-cache" \
uv tool run --python /cm/local/apps/python3/bin/python3 --from pre-commit \
  pre-commit run --all-files
git diff --check

Validation scripts, logs, JUnit XML, coverage, and rollout evidence are local artifacts, not committed source. They are retained under:

/scratch/fsw/portfolios/nemotron/projects/nemotron_sw_post/users/pthombre/GymPR/Gym/.worktrees/capture-worker-bypass/cache/worker-bypass-validation/

Key artifacts: results.json, baseline.xml, regressions.xml, pre-commit-all-files.log, and real-rollout/evidence.json.

Rollouts

Slurm GPU job 3966328 completed successfully using Qwen/Qwen3-0.6B, vLLM 0.25.1, and the real NeMo RL HTTP worker with local durable staging, served through Gym Responses SSE:

  • A two-turn add(2, 2) tool rollout returned 4, passed the smoke verifier (reward 1.0), and produced two chained committed calls: 234 total tokens, including 27 training tokens, with finite logprobs.
  • A subsequent real backend context-overflow error left those committed records and the reconstructed training row unchanged, adding only the uncommitted-call record.
  • A separate reasoning-only generation followed by the reasoning guard retained all 16 generated tokens, again adding only the uncommitted-call record.

This is a vLLM real-model smoke test; Megatron behavior is covered by the parameterized handler and route tests, not a live Megatron deployment.

Compatibility and benchmark impact

No public API, configuration, ledger/staging schema, or migration changes. Synthetic API responses keep their existing finish reasons. Valid earlier generations can remain trainable when the rollout ends in a synthetic completion, eliminating this source of dropped training rows. Missing acknowledgements from actual worker completions still fail closed. Benchmark scoring logic is unchanged.

Documentation: N/A; this restores existing capture behavior without adding user configuration or a public interface.

Checklist

  • I have read the contributing guidelines.
  • The change is focused; no unrelated edits.
  • Tests added and the focused suite passes.
  • Documentation assessed: N/A, as explained above.
  • pre-commit run --all-files passes.
  • No new source files; existing SPDX headers retained.
  • All commits have DCO sign-off.

Track normal worker completions per request so token-limit and reasoning-guard responses remain uncommitted without poisoning earlier captured turns. Preserve acknowledgement validation for actual worker responses across vLLM and Megatron.

Add 44 regression cases across handlers, API routes, and JSON/SSE. Validation: 1,055 focused tests and real vLLM rollouts pass in Slurm; all-files pre-commit passes.

Signed-off-by: Pranav Thombre <pthombre@nvidia.com>
@pthombre pthombre added bug Something isn't working area:training Training framework integrations and training-data interfaces labels Sep 23, 2026
@copy-pr-bot

copy-pr-bot Bot commented Sep 23, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@pthombre

Copy link
Copy Markdown
Contributor Author

/ok to test 3c8f7ba

@pthombre
pthombre marked this pull request as ready for review September 24, 2026 00:40
@pthombre
pthombre requested a review from ananthsub September 24, 2026 00:41
@yaoyu-33 yaoyu-33 added complexity:low Localized change in one scope with a small, straightforward review surface needs-review PR is ready for code review and waiting on a reviewer labels Sep 24, 2026
@github-actions github-actions Bot added the sla:review-overdue Review response is over the one-business-day SLA label Sep 25, 2026
ananthsub
ananthsub previously approved these changes Sep 28, 2026
Comment thread nemo_gym/token_id_capture/external_capture.py
@github-actions github-actions Bot removed the sla:review-overdue Review response is over the one-business-day SLA label Sep 28, 2026
Address review on #3670. Document in the ExternalCaptureHandler protocol that
prepare_response() must run for every worker completion, since
finalize_response() now commits or poisons a call only after that mark is set.
Reword the early-return comment to distinguish synthetic completions (no worker
completion at all) from a worker completion whose acknowledgement is missing.

Add a route-level regression test that drops ng_commit_coords from a real
worker completion and asserts the call still fails closed with
worker_response_missing_commit_coordinates, for both backends with and without
streaming.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Pranav Thombre <pthombre@nvidia.com>
@pthombre

pthombre commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

/ok to test 22761ca

1 similar comment
@pthombre

Copy link
Copy Markdown
Contributor Author

/ok to test 22761ca

@ananthsub
ananthsub enabled auto-merge September 29, 2026 23:09
@ananthsub
ananthsub added this pull request to the merge queue Sep 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/36644628896

Merged via the queue into main with commit f7816f9 Sep 29, 2026
42 checks passed
@ananthsub
ananthsub deleted the pranav/vllm_token_limit_fix branch September 29, 2026 23:34

This branch was successfully deployed

1 active deployment
public — 22761ca5 Deployed Sep 29, 2026 by copy-pr-bot[bot] via release / finalize / notify #3989
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:training Training framework integrations and training-data interfaces bug Something isn't working complexity:low Localized change in one scope with a small, straightforward review surface needs-review PR is ready for code review and waiting on a reviewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants